import dash
from dash import dcc, html, Input, Output
import geopandas as gpd
import pandas as pd
import plotly.graph_objects as go
# Load the shapefile containing Roman roads
roads_gdf = gpd.read_file('roman_roads_v2008.shp')
# Convert coordinate reference system to EPSG:4326 (WGS 84)
roads_gdf = roads_gdf.to_crs('EPSG:4326')
# Simplify geometries to reduce the number of points and improve performance.
# First, project to a metric CRS (EPSG:3857), simplify with a tolerance in meters,
# then convert back to EPSG:4326.
roads_gdf['geometry'] = roads_gdf.geometry.to_crs("EPSG:3857") \
.simplify(tolerance=1000, preserve_topology=True) \
.to_crs("EPSG:4326")
# Compute an approximate boundary of the Roman Empire by taking the convex hull
# of all the Roman roads. This will be used later to filter ports.
roman_boundary = roads_gdf.unary_union.convex_hull
# Load the ancient ports CSV file
ports_df = pd.read_excel('AncientPorts_Europe.xlsx')
# Create a GeoDataFrame for ports with EPSG:4326 CRS
ports_gdf = gpd.GeoDataFrame(
ports_df,
geometry=gpd.points_from_xy(
ports_df['LONGITUDE'],
ports_df['LATITUDE']
),
crs='EPSG:4326'
)
# Filter ports to only those that fall within the approximate Roman Empire boundary.
ports_gdf = ports_gdf[ports_gdf.within(roman_boundary)]
# Define colors and styles for road classification
color_dict = {
'Major Road': 'dodgerblue',
'Minor Road': 'silver'
}
roman_cities = pd.DataFrame({
"City": [
"Rome", "Carthage", "Londinium", "Constantinople", "Alexandria", "Antioch",
"Eburacum", "Ravenna", "Mediolanum", "Massilia", "Colonia Agrippina", "Aquincum",
"Sirmium", "Emerita Augusta", "Tarraco", "Vindobona", "Jerusalem", "Caesarea",
"Tauromenium", "Berytus", "Narbo Martius", "Patavium", "Neapolis",
"Lutetia", "Lugdunum", "Cartagena", "Nîmes", "Arles", "Timgad", "Leptis Magna",
"Thessalonica", "Ephesus", "Pergamum", "Brindisium", "Augusta Treverorum",
"Viminacium", "Dura-Europos", "Verulamium", "Cyrene", "Syracuse", "Batavodurum, later Noviomagus", "Traiectum", "Caerleon", "Deva Victrix", "Tolosa",
"Argentoratum", "Burdigala", "Hispalis", "Olisipo", "Malaca", "Diocaesarea",
"Tiberias", "Edessa", "Aquileia", "Mogontiacum"
],
"Province": [
"Italia", "Africa", "Britannia", "Asia", "Aegyptus", "Syria",
"Britannia", "Italia", "Italia", "Gallia", "Germania Inferior", "Pannonia",
"Pannonia", "Lusitania", "Hispania Tarraconensis", "Pannonia", "Palestina", "Judaea",
"Sicilia", "Phoenice", "Gallia Narbonensis", "Italia", "Italia",
"Gallia Lugdunensis", "Gallia Lugdunensis", "Hispania Carthaginensis", "Gallia Narbonensis",
"Gallia Narbonensis", "Numidia", "Africa Proconsularis", "Macedonia", "Asia", "Asia",
"Italia", "Gallia Belgica", "Moesia", "Syria", "Britannia", "Cyrenaica", "Sicilia", "Germania Inferior", "Germania Inferior", "Britannia", "Britannia", "Gallia Narbonensis",
"Germania Superior", "Gallia Aquitania", "Hispania Baetica", "Hispania Lusitania", "Hispania Baetica", "Judaea",
"Judaea", "Osroene", "Italia", "Germania Superior"
],
"Lat": [
41.9028, 36.8528, 51.5074, 41.0082, 31.2001, 36.2021,
53.9590, 44.4142, 45.4642, 43.2965, 50.9375, 47.4980,
45.0038, 38.9160, 41.1189, 48.2082, 31.7683, 32.5000,
37.8530, 33.8938, 43.1840, 45.4064, 40.8518,
48.8566, 45.7640, 37.6050, 43.8367, 43.6766, 35.4875, 32.6396,
40.6401, 37.9390, 39.1233, 40.6333, 49.75, 44.0167, 34.7878,
51.75, 32.124, 37.0755, 51.84, 52.09, 51.61, 53.19, 43.60,
48.58, 44.84, 37.39, 38.72, 36.72, 32.78,
32.79, 37.16, 45.83, 50.00
],
"Lon": [
12.4964, 10.3231, -0.1278, 28.9784, 29.9187, 37.1343,
-1.0815, 12.1960, 9.1900, 5.3698, 6.9603, 19.0402,
19.6120, -6.3430, 1.2445, 16.3738, 35.2137, 34.8920,
15.2860, 35.5018, 3.0060, 11.8768, 14.2681,
2.3522, 4.8357, -0.9869, 4.3601, 4.6280, 6.4689, 14.2904,
22.9444, 27.3417, 27.1833, 17.9333, 6.6333, 21.3000, 40.9964,
-0.3333, 21.769, 15.2866, 5.84, 5.11, -3.00, -2.93, 1.44,
7.75, -0.57, -5.99, -9.14, -4.42, 35.25,
35.54, 38.79, 13.44, 8.27
],
"Founding": [
"753 BC", "814 BC", "AD 47", "AD 330", "331 BC", "c. 300 BC",
"AD 71", "c. 500 BC", "c. 600 BC", "c. 600 BC", "AD 50", "AD 89",
"c. 300 BC", "25 BC", "218 BC", "AD 15", "c. 3000 BC", "22 BC",
"c. 396 BC", "15 BC", "118 BC", "c. 400 BC", "c. 600 BC",
"c. 250 BC", "43 BC", "227 BC", "470 BC", "46 BC", "AD 100", "c. 1000 BC",
"315 BC", "c. 10th century BC", "c. 300 BC", "254 BC", "16 BC",
"AD 29", "303 BC", "AD 50", "631 BC", "8th century BC",
"c. 19 BC", "AD 47", "AD 75", "AD 79", "c. 120 BC",
"c. 12 BC", "c. 60 BC", "c. 219 BC", "c. 1200 BC", "c. 300 BC", "c. 200 BC",
"c. 20 CE", "c. 400 BC", "181 BC", "AD 13"
],
"Population": [
"2.8 million", "Tunis: ~638,000", "9 million", "15 million", "5 million", "150,000",
"210,000", "160,000", "1.3 million", "861,000", "1.1 million", "1.75 million",
"35,000", "60,000", "130,000", "1.9 million", "900,000", "3,000",
"11,000", "2.2 million", "53,000", "210,000", "3 million",
"2.1 million", "515,000", "215,000", "150,000", "53,000", "N/A", "150,000",
"1 million", "N/A", "70,000", "88,000", "115,000", "N/A", "N/A",
"147,000", "40,000", "120,000", "approx 40,000", "approx 50,000", "approx 20,000", "approx 25,000", "approx 80,000",
"approx 100,000", "approx 150,000", "approx 200,000", "approx 250,000", "approx 150,000", "approx 30,000",
"approx 20,000", "approx 50,000", "approx 100,000", "approx 40,000"
],
"Modern": [
"Rome", "Tunis", "London", "Istanbul", "Alexandria", "Antakya",
"York", "Ravenna", "Milan", "Marseille", "Cologne", "Budapest",
"Sremska Mitrovica", "Mérida", "Tarragona", "Vienna", "Jerusalem", "Caesarea",
"Taormina", "Beirut", "Narbonne", "Padua", "Naples",
"Paris", "Lyon", "Cartagena", "Nîmes", "Arles", "Timgad", "Al Khums",
"Thessaloniki", "Selçuk", "Bergama", "Brindisi", "Trier",
"Archaeological site near Kostolac", "Archaeological site near Deir ez-Zor", "St Albans", "Shahhat", "Syracuse",
"Nijmegen", "Utrecht", "Caerleon", "Chester", "Toulouse",
"Strasbourg", "Bordeaux", "Seville", "Lisbon", "Málaga", "Sepphoris",
"Tiberias", "Edessa", "Aquileia", "Mainz"
]
})
roman_cities['Hover'] = (
roman_cities['City'] + ' (' + roman_cities['Province'] + '), ' +
'Founding: ' + roman_cities['Founding'] + ', ' +
'Population: ' + roman_cities['Population'] + ', ' +
'Modern: ' + roman_cities['Modern']
)
app = dash.Dash(__name__)
# Create dropdown options for road classes based on unique values in the original roads dataset.
road_class_options =[
{'label': rc, 'value': rc} for rc in roads_gdf['CLASS'].unique()
]
# Set up the layout with controls and a graph.
app.layout = html.Div([
html.H2('Roman Roads Dashboard'),
html.Div([
html.Label('Select Road Classes:'),
dcc.Dropdown(
id='road-class-dropdown',
options=road_class_options,
value=roads_gdf['CLASS'].unique().tolist(), # default to all classes
multi=True
)
], style={'width': '30%', 'display': 'inline-block', 'verticalAlign': 'top'}),
dcc.Graph(id='map-graph')
])
@app.callback(
Output('map-graph', 'figure'),
Input('road-class-dropdown', 'value')
)
def update_map(selected_classes):
# Filter roads based on the selected classes.
filtered = roads_gdf[roads_gdf['CLASS'].isin(selected_classes)].copy()
# Apply fixed geometry simplification (tolerance set to 1000 meters).
filtered['simp_geom'] = filtered.geometry.to_crs('EPSG:3857').simplify(
tolerance=1000, preserve_topology=True
).to_crs('EPSG:4326')
# Compute centroids and midpoints.
filtered['centroid'] = filtered.geometry.centroid
filtered['midpoint'] = filtered['simp_geom'].apply(lambda geom: geom.interpolate(0.5, normalized=True))
# Create hover text for each road (with road length rounded to an integer).
filtered['hover_text'] = (
'Road Type: ' + filtered['CLASS'].astype(str) + ', ' +
'Length: ' + filtered['Shape_Leng'].round(0).astype(int).astype(str) + ', ' +
'Source: ' + filtered['SOURCE'].astype(str)
)
# Reassign styling parameters based on road type.
filtered['color'] = filtered['CLASS'].map(color_dict)
filtered['opacity'] = filtered['CLASS'].apply(lambda x: 1.0 if x == 'Major Road' else 0.7)
filtered['width'] = filtered['CLASS'].apply(lambda x: 0.9 if x == 'Major Road' else 0.7)
# Aggregate Roads by Class
traces = []
for road_class, group in filtered.groupby('CLASS'):
all_lats = []
all_lons = []
for idx, row in group.iterrows():
coords = list(row['simp_geom'].coords)
all_lats.extend([pt[1] for pt in coords] + [None])
all_lons.extend([pt[0] for pt in coords] + [None])
style_color = group.iloc[0]['color']
style_width = group.iloc[0]['width']
style_opacity = group.iloc[0]['opacity']
traces.append(go.Scattermapbox(
lat=all_lats,
lon=all_lons,
mode='lines',
line=dict(
color=style_color,
width=style_width
),
opacity=style_opacity,
hoverinfo='none',
name=road_class
))
# Add invisible markers at road midpoints for detailed hover information.
traces.append(go.Scattermapbox(
lat=filtered['midpoint'].apply(lambda point: point.y),
lon=filtered['midpoint'].apply(lambda point: point.x),
mode='markers',
marker=dict(
size=4,
color='rgba(0,0,0,0)'
),
text=filtered['hover_text'],
hovertemplate='%{text}<extra></extra>',
name='Road Details'
))
# Add Ancient Ports Layer
ports_trace = go.Scattermapbox(
lat=ports_gdf.geometry.y,
lon=ports_gdf.geometry.x,
mode='markers',
marker=dict(
size=5,
color='yellow'
),
text=ports_gdf['NAME'],
hovertemplate='Port: %{text}<extra></extra>',
name='Ancient Ports'
)
traces.append(ports_trace)
# Add Roman Cities Layer
cities_trace = go.Scattermapbox(
lat=roman_cities['Lat'],
lon=roman_cities['Lon'],
mode='markers',
marker=dict(
size=8,
color='red'
),
text=roman_cities['Hover'],
hovertemplate='%{text}<extra></extra>',
hoverlabel=dict(bgcolor='red'),
name='Roman Cities'
)
traces.append(cities_trace)
# Create and Center the Map
fig = go.Figure(data=traces)
if len(filtered) > 0:
center_lat = filtered['centroid'].y.mean()
center_lon = filtered['centroid'].x.mean()
else:
center_lat, center_lon = ((lat_min + lat_max) / 2, (lon_min + lon_max) / 2)
fig.update_layout(
mapbox_style='carto-darkmatter',
mapbox_zoom=4,
mapbox_center={
'lat': center_lat,
'lon': center_lon},
height=600,
margin=dict(
l=0,
r=0,
t=0,
b=0
)
)
return fig
if __name__ == '__main__':
app.run_server(debug=True)